//*********************************************************
//
//    Copyright (c) Microsoft. All rights reserved.
//    This code is licensed under the Apache License, Version 2.0.
//    THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
//    ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
//    IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
//    PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
//
//*********************************************************

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

namespace Microsoft.Research.DataLayer
{
    /// <summary>
    /// Common information for all blob objects
    /// </summary>
    public interface IBlob
    {
        /// <summary>
        /// Index of this chunk
        /// </summary>
        int Index { get; }

        /// <summary>
        /// Total number of chunks for blob stream
        /// </summary>
        int Chunks { get; }

        /// <summary>
        /// Blob contents (BASE64 encoded)
        /// </summary>
        string Contents { get; set; }

        /// <summary>
        /// ID of the blob (same for all chunks)
        /// </summary>
        Guid PackageID { get; }

        /// <summary>
        /// ID of the blob (version) (same for all chunks)
        /// </summary>
        Guid RevisionID { get; }
    }

    /// <summary>
    /// Helper class to handle blobs in the storage
    /// </summary>
    public sealed class BlobBase
    {
        private const int BLOB_CHUNK_SIZE = 16384;

        /// <summary>
        /// Direction of transfer
        /// </summary>
        public enum TransferDirection
        {
            /// <summary>
            /// Blob is being downloaded
            /// </summary>
            Download,

            /// <summary>
            /// Blob is being uploaded
            /// </summary>
            Upload,
        }

        /// <summary>
        /// Delegate to handle clearing the store (begin)
        /// </summary>
        /// <param name="owner"></param>
        /// <param name="field"></param>
        /// <param name="conn"></param>
        public delegate void BeginClearStoreProgressHandler(object owner, string field, Connection conn);

        /// <summary>
        /// Delegate to handle clearing the store (end)
        /// </summary>
        /// <param name="owner"></param>
        /// <param name="field"></param>
        /// <param name="conn"></param>
        public delegate void EndClearStoreProgressHandler(object owner, string field, Connection conn);

        /// <summary>
        /// Delegate to handle progress events
        /// </summary>
        /// <param name="dir"></param>
        /// <param name="curChunk"></param>
        /// <param name="maxChunks"></param>
        /// <param name="curChunkLen"></param>
        /// <param name="chunkSize"></param>
        /// <param name="owner"></param>
        /// <param name="field"></param>
        /// <param name="obj"></param>
        /// <param name="conn"></param>
        public delegate void TransferProgressHandler(TransferDirection dir, object owner, object obj, string field,
            long curChunk, long maxChunks, long curChunkLen, long chunkSize, Connection conn);

        /// <summary>
        /// Creates a basic blob object
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <typeparam name="O"></typeparam>
        /// <param name="streamId"></param>
        /// <param name="index"></param>
        /// <param name="total"></param>
        /// <param name="revisionId"></param>
        /// <param name="src"></param>
        /// <param name="c"></param>
        /// <returns></returns>
        public delegate T BlobCreator<T, O>(Guid streamId, int index, int total, Guid revisionId, O src, Connection c);


        /// <summary>
        /// Fired on any blob transfer
        /// </summary>
        public static event TransferProgressHandler OnAnyBlobTransfer;

        /// <summary>
        /// Fired on any beginning of store cleanup
        /// </summary>
        public static event BeginClearStoreProgressHandler OnAnyBeginClearStore;


        /// <summary>
        /// Fired after any store cleanup end
        /// </summary>
        public static event EndClearStoreProgressHandler OnAnyEndClearStore;


        /// <summary>
        /// Helper to fire events of cleaning store
        /// </summary>
        /// <param name="owner"></param>
        /// <param name="field"></param>
        /// <param name="conn"></param>
        public static void FireOnAnyBeginClearStore(object owner, string field, Connection conn)
        {
            if (OnAnyBeginClearStore != null)
                OnAnyBeginClearStore(owner, field, conn);
        }

        /// <summary>
        /// Helper to fire events of cleaning store
        /// </summary>
        /// <param name="owner"></param>
        /// <param name="field"></param>
        /// <param name="conn"></param>
        public static void FireOnAnyEndClearStore(object owner, string field, Connection conn)
        {
            if (OnAnyEndClearStore != null)
                OnAnyEndClearStore(owner, field, conn);
        }

        /// <summary>
        /// Fetch blob chunks and return memory stream of assembled image
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <typeparam name="O"></typeparam>
        /// <param name="list"></param>
        /// <returns></returns>
        public static System.IO.Stream FetchBlobs<T, O>(O list)
            where T : IBlob
            where O : List<IBlob>
        {
            // Reassemble original stream
            MemoryStream bStream = new MemoryStream();
            Stream sStream = bStream as Stream;
            return FetchBlobs<T, O>(list, ref sStream);
        }

        /// <summary>
        /// Fetch blob chunks and return memory stream of assembled image
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <typeparam name="O"></typeparam>
        /// <param name="list"></param>
        /// <returns></returns>
        public static System.IO.Stream FetchBlobs<T, O>(O list, ref Stream bStream)
            where T : IBlob
            where O : List<IBlob>
        {
            int count = list.Count;

            if (count == 0)
                return null;

            //
            // First, let's do sanity checks
            //

            Guid packageId = list[0].PackageID;
            Guid revisionId = list[0].RevisionID;

            long streamPos = bStream.Position;

            T[] chunks = new T[count];

            foreach (T b in list)
            {
                chunks[b.Index] = b;
            }

            // Reassemble original stream
            foreach (T b in chunks)
            {
                // All should have same packageId and revisionId
                if (b.PackageID != packageId || b.RevisionID != revisionId)
                    throw new ApplicationException("Data mismatch");

                // They all should refer to the same chunk size
                if (b.Chunks != count)
                    throw new ApplicationException("Size mismatch");

                // We need every piece together
                if (b.Index >= count)
                    throw new ApplicationException("Alien packet piece");

                // Put the piece into its original place
                // Deserialize image
                byte[] bContent = Convert.FromBase64String(b.Contents);
                bStream.Write(bContent, 0, bContent.Length);
            }

            bStream.Seek(streamPos, System.IO.SeekOrigin.Begin);

            return bStream;
        }

        /// <summary>
        /// Breaks blob into chunks and save them into the store
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <typeparam name="O"></typeparam>
        /// <param name="src"></param>
        /// <param name="obj"></param>
        /// <param name="field"></param>
        /// <param name="contents"></param>
        /// <param name="creator"></param>
        /// <param name="c"></param>
        /// <param name="progress"></param>
        /// <returns></returns>
        public static List<T> SaveBlobs<T, O>(O src, object obj, string field, byte[] contents, BlobCreator<T, O> creator,
            TransferProgressHandler progress, Connection c)
            where T : IBlob
        {
            List<T> results = new List<T>();

            int chunks = (int)Math.Ceiling(1.0 * contents.Length / BLOB_CHUNK_SIZE);

            Guid streamId = Guid.NewGuid();
            Guid revisionId = Guid.NewGuid();

            int round = 0;
            long index = 0;
            long rest = contents.Length;
            byte[] buffer = new byte[BLOB_CHUNK_SIZE];
            while (rest > 0)
            {
                long roundLen = Math.Min(BLOB_CHUNK_SIZE, rest);

                if (OnAnyBlobTransfer != null)
                    OnAnyBlobTransfer(TransferDirection.Upload, src, obj, field, round, chunks, roundLen, BLOB_CHUNK_SIZE, c);
                if (progress != null)
                    progress(TransferDirection.Upload, src, obj, field, round, chunks, roundLen, BLOB_CHUNK_SIZE, c);

                Array.Copy(contents, index, buffer, 0, roundLen);
                string chunkContents = Convert.ToBase64String(buffer, 0, (int)roundLen);

                // Creates the blob
                T chunkBlob = creator(streamId, round, chunks, revisionId, src, c);
                chunkBlob.Contents = chunkContents;

                results.Add(chunkBlob);

                round++;
                index += roundLen;
                rest -= roundLen;
            }

            return results;
        }

        /// <summary>
        /// Utility function to convert from MemoryStream to byte[]
        /// </summary>
        /// <param name="ms"></param>
        /// <returns></returns>
        public static byte[] StreamToByteArray(System.IO.Stream ms)
        {
            byte[] ret = new byte[ms.Length];
            ms.Read(ret, 0, ret.Length);
            return ret;
        }
    }
}
